// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use brotli::enc::backward_references::BrotliEncoderMode;
use brotli::enc::encode::BrotliEncoderCompress;
use brotli::enc::encode::BrotliEncoderOperation;
use brotli::enc::encode::BrotliEncoderParameter;
use brotli::enc::encode::BrotliEncoderStateStruct;
use brotli::writer::StandardAlloc;
use brotli::BrotliDecompressStream;
use brotli::BrotliResult;
use brotli::BrotliState;
use brotli::Decompressor;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::op2;
use deno_core::JsBuffer;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ToJsBuffer;
use std::cell::RefCell;
use std::io::Read;

fn encoder_mode(mode: u32) -> Result<BrotliEncoderMode, AnyError> {
    Ok(match mode {
        0 => BrotliEncoderMode::BROTLI_MODE_GENERIC,
        1 => BrotliEncoderMode::BROTLI_MODE_TEXT,
        2 => BrotliEncoderMode::BROTLI_MODE_FONT,
        3 => BrotliEncoderMode::BROTLI_FORCE_LSB_PRIOR,
        4 => BrotliEncoderMode::BROTLI_FORCE_MSB_PRIOR,
        5 => BrotliEncoderMode::BROTLI_FORCE_UTF8_PRIOR,
        6 => BrotliEncoderMode::BROTLI_FORCE_SIGNED_PRIOR,
        _ => return Err(type_error("Invalid encoder mode")),
    })
}

#[op2(fast)]
#[number]
pub fn op_brotli_compress(
    #[buffer] buffer: &[u8],
    #[buffer] out: &mut [u8],
    #[smi] quality: i32,
    #[smi] lgwin: i32,
    #[smi] mode: u32,
) -> Result<usize, AnyError> {
    let mode = encoder_mode(mode)?;
    let mut out_size = out.len();

    let result = BrotliEncoderCompress(
        StandardAlloc::default(),
        &mut StandardAlloc::default(),
        quality,
        lgwin,
        mode,
        buffer.len(),
        buffer,
        &mut out_size,
        out,
        &mut |_, _, _, _| (),
    );
    if result != 1 {
        return Err(type_error("Failed to compress"));
    }

    Ok(out_size)
}

fn max_compressed_size(input_size: usize) -> usize {
    if input_size == 0 {
        return 2;
    }

    // [window bits / empty metadata] + N * [uncompressed] + [last empty]
    let num_large_blocks = input_size >> 14;
    let overhead = 2 + (4 * num_large_blocks) + 3 + 1;
    let result = input_size + overhead;

    if result < input_size {
        0
    } else {
        result
    }
}

#[op2(async)]
#[serde]
pub async fn op_brotli_compress_async(
    #[buffer] input: JsBuffer,
    #[smi] quality: i32,
    #[smi] lgwin: i32,
    #[smi] mode: u32,
) -> Result<ToJsBuffer, AnyError> {
    let mode = encoder_mode(mode)?;
    tokio::task::spawn_blocking(move || {
        let input = &*input;
        let mut out = vec![0u8; max_compressed_size(input.len())];
        let mut out_size = out.len();

        let result = BrotliEncoderCompress(
            StandardAlloc::default(),
            &mut StandardAlloc::default(),
            quality,
            lgwin,
            mode,
            input.len(),
            input,
            &mut out_size,
            &mut out,
            &mut |_, _, _, _| (),
        );
        if result != 1 {
            return Err(type_error("Failed to compress"));
        }

        out.truncate(out_size);
        Ok(out.into())
    })
    .await?
}

struct BrotliCompressCtx {
    inst: RefCell<BrotliEncoderStateStruct<StandardAlloc>>,
}

impl Resource for BrotliCompressCtx {}

#[op2]
#[smi]
pub fn op_create_brotli_compress(state: &mut OpState, #[serde] params: Vec<(u8, i32)>) -> u32 {
    let mut inst = BrotliEncoderStateStruct::new(StandardAlloc::default());

    for (key, value) in params {
        inst.set_parameter(encoder_param(key), value as u32);
    }

    state.resource_table.add(BrotliCompressCtx {
        inst: RefCell::new(inst),
    })
}

fn encoder_param(param: u8) -> BrotliEncoderParameter {
    // SAFETY: BrotliEncoderParam is valid for 0-255
    unsafe { std::mem::transmute(param as u32) }
}

#[op2(fast)]
#[number]
pub fn op_brotli_compress_stream(
    state: &mut OpState,
    #[smi] rid: u32,
    #[buffer] input: &[u8],
    #[buffer] output: &mut [u8],
) -> Result<usize, AnyError> {
    let ctx = state.resource_table.get::<BrotliCompressCtx>(rid)?;
    let mut inst = ctx.inst.borrow_mut();
    let mut output_offset = 0;

    let result = inst.compress_stream(
        BrotliEncoderOperation::BROTLI_OPERATION_PROCESS,
        &mut input.len(),
        input,
        &mut 0,
        &mut output.len(),
        output,
        &mut output_offset,
        &mut None,
        &mut |_, _, _, _| (),
    );
    if !result {
        return Err(type_error("Failed to compress"));
    }

    Ok(output_offset)
}

#[op2(fast)]
#[number]
pub fn op_brotli_compress_stream_end(
    state: &mut OpState,
    #[smi] rid: u32,
    #[buffer] output: &mut [u8],
) -> Result<usize, AnyError> {
    let ctx = state.resource_table.get::<BrotliCompressCtx>(rid)?;
    let mut inst = ctx.inst.borrow_mut();
    let mut output_offset = 0;

    let result = inst.compress_stream(
        BrotliEncoderOperation::BROTLI_OPERATION_FINISH,
        &mut 0,
        &[],
        &mut 0,
        &mut output.len(),
        output,
        &mut output_offset,
        &mut None,
        &mut |_, _, _, _| (),
    );
    if !result {
        return Err(type_error("Failed to compress"));
    }

    Ok(output_offset)
}

fn brotli_decompress(buffer: &[u8]) -> Result<ToJsBuffer, AnyError> {
    let mut output = Vec::with_capacity(4096);
    let mut decompressor = Decompressor::new(buffer, buffer.len());
    decompressor.read_to_end(&mut output)?;
    Ok(output.into())
}

#[op2]
#[serde]
pub fn op_brotli_decompress(#[buffer] buffer: &[u8]) -> Result<ToJsBuffer, AnyError> {
    brotli_decompress(buffer)
}

#[op2(async)]
#[serde]
pub async fn op_brotli_decompress_async(
    #[buffer] buffer: JsBuffer,
) -> Result<ToJsBuffer, AnyError> {
    tokio::task::spawn_blocking(move || brotli_decompress(&buffer)).await?
}

struct BrotliDecompressCtx {
    inst: RefCell<BrotliState<StandardAlloc, StandardAlloc, StandardAlloc>>,
}

impl Resource for BrotliDecompressCtx {}

#[op2(fast)]
#[smi]
pub fn op_create_brotli_decompress(state: &mut OpState) -> u32 {
    let inst = BrotliState::new(
        StandardAlloc::default(),
        StandardAlloc::default(),
        StandardAlloc::default(),
    );
    state.resource_table.add(BrotliDecompressCtx {
        inst: RefCell::new(inst),
    })
}

#[op2(fast)]
#[number]
pub fn op_brotli_decompress_stream(
    state: &mut OpState,
    #[smi] rid: u32,
    #[buffer] input: &[u8],
    #[buffer] output: &mut [u8],
) -> Result<usize, AnyError> {
    let ctx = state.resource_table.get::<BrotliDecompressCtx>(rid)?;
    let mut inst = ctx.inst.borrow_mut();
    let mut output_offset = 0;

    let result = BrotliDecompressStream(
        &mut input.len(),
        &mut 0,
        input,
        &mut output.len(),
        &mut output_offset,
        output,
        &mut 0,
        &mut inst,
    );
    if matches!(result, BrotliResult::ResultFailure) {
        return Err(type_error("Failed to decompress"));
    }

    Ok(output_offset)
}

#[op2(fast)]
#[number]
pub fn op_brotli_decompress_stream_end(
    state: &mut OpState,
    #[smi] rid: u32,
    #[buffer] output: &mut [u8],
) -> Result<usize, AnyError> {
    let ctx = state.resource_table.get::<BrotliDecompressCtx>(rid)?;
    let mut inst = ctx.inst.borrow_mut();
    let mut output_offset = 0;

    let result = BrotliDecompressStream(
        &mut 0,
        &mut 0,
        &[],
        &mut output.len(),
        &mut output_offset,
        output,
        &mut 0,
        &mut inst,
    );
    if matches!(result, BrotliResult::ResultFailure) {
        return Err(type_error("Failed to decompress"));
    }

    Ok(output_offset)
}
